OS (28) 분산 파일 시스템의 확장성 혁명: AFS(Andrew File System) 완벽 분석
분산 파일 시스템의 확장성 혁명: AFS(Andrew File System) 완벽 분석
분산 파일 시스템(Distributed File System)을 설계할 때 가장 큰 난제는 무엇일까요? 바로 확장성(Scalability) 입니다. 클라이언트가 수십 대일 때는 문제가 없던 프로토콜도, 수천 대가 되면 서버의 병목 현상으로 인해 시스템 전체가 마비될 수 있습니다.
오늘은 1980년대 Carnegie Mellon University(CMU)에서 개발되어 분산 시스템의 역사를 새로 쓴 Andrew File System(AFS) 에 대해 깊이 파고들어 보겠습니다. AFS는 NFS(Network File System)가 가진 확장성의 한계를 극복하기 위해 설계되었으며, 그 설계 철학은 현대의 분산 시스템에도 여전히 큰 영감을 주고 있습니다.
이 글은 AFS의 초기 버전(v1)의 실패 원인부터, 이를 개선한 AFSv2의 핵심 메커니즘인 콜백(Callback), 그리고 캐시 일관성(Cache Consistency) 과 성능 분석 까지 망라합니다.
1. 서론: 왜 AFS인가?
AFS 프로젝트의 리더인 M. Satyanarayanan(Satya) 교수는 핵심 목표를 확장성 에 두었습니다. "가능한 많은 클라이언트를 지원하려면 분산 파일 시스템을 어떻게 설계해야 하는가?"가 그들의 질문이었습니다.
기존의 NFS 프로토콜은 상태를 저장하지 않는(Stateless) 구조 덕분에 복구는 쉬웠지만, 확장성에는 치명적인 약점이 있었습니다. 클라이언트는 캐시된 데이터가 최신인지 확인하기 위해 주기적으로 서버에 물어봐야 했기 때문입니다(Polling). 이는 서버의 CPU와 네트워크 대역폭을 지속적으로 소모하게 만들어, 단일 서버가 감당할 수 있는 클라이언트 수를 제한했습니다.
AFS는 이러한 "상식적인 문제"를 해결하고, 사용자에게 로컬 파일 시스템과 구분이 가지 않는 성능을 제공하는 것을 목표로 했습니다.
2. AFSv1: 초기 설계와 실패, 그리고 교훈
2.1 AFSv1의 기본 원칙: 전체 파일 캐싱 (Whole-File Caching)
AFSv1은 아주 단순하면서도 강력한 원칙을 세웠습니다. 클라이언트가 파일을 열면(open), 서버에서 파일 전체를 가져와 로컬 디스크에 저장한다.
이 방식의 장점은 명확합니다.
- 로컬 성능: 일단 파일을 가져오면, 이후의 모든
read()와write()연산은 로컬 디스크에서 이루어집니다. 네트워크 통신이 발생하지 않으므로 매우 빠릅니다. - 서버 부하 감소:
read/write단위로 통신하는 NFS와 달리, 파일을 열고 닫을 때만 서버와 통신합니다.
파일을 닫을 때(close()) 변경 사항이 있다면, 파일 전체를 다시 서버로 전송하여 저장합니다.
2.2 AFSv1의 프로토콜과 문제점
하지만 AFSv1은 실패했습니다. CMU 연구진은 Patterson's Law ("측정을 먼저 하고 만들어라")에 따라 시스템을 분석했고, 두 가지 치명적인 병목을 발견했습니다.
-
경로명 탐색 비용 (Path Traversal Cost): 클라이언트가
/home/woogie/project/main.c라는 파일을 요청하면, 서버는 루트에서부터home,woogie,project디렉터리를 하나씩 탐색해야 했습니다. 많은 클라이언트가 동시에 요청을 보내자, 서버 CPU 시간의 대부분이 단순히 경로를 따라가는 데 소모되었습니다. -
TestAuth 메시지의 폭주: NFS와 비슷하게, AFSv1 클라이언트도 로컬에 캐시된 파일이 유효한지 확인하기 위해 서버에
TestAuth메시지를 너무 자주 보냈습니다. 대부분의 경우 "변경 없음"이라는 응답을 받았지만, 이 확인 작업 자체가 서버를 과부하 상태로 만들었습니다. 결과적으로 서버는 고작 20대의 클라이언트도 감당하기 힘들었습니다.
3. AFSv2: 확장성을 위한 재설계
연구진은 앞서 발견한 두 가지 문제를 해결하기 위해 프로토콜을 완전히 뜯어고쳤습니다. 이것이 바로 우리가 공부해야 할 핵심인 AFSv2 입니다.
3.1 핵심 솔루션 1: 콜백(Callback)
AFSv2는 클라이언트가 서버에게 계속 물어보는(Polling) 대신, 서버가 클라이언트에게 알려주는(Interrupt 유사) 방식을 도입했습니다. 이를 콜백(Callback) 이라고 합니다.
- 동작 원리: 클라이언트가 서버에서 파일을 가져올 때, 서버는 해당 파일에 대해 이 파일이 변경되면 너에게 꼭 알려줄게 라는 약속(Callback)을 함께 줍니다.
- 효과: 클라이언트는 서버로부터 연락이 없으면, 자신의 로컬 캐시에 있는 파일이 최신이라고 100% 확신할 수 있습니다. 따라서
TestAuth같은 확인 메시지를 보낼 필요가 없어졌습니다. 서버와의 통신이 획기적으로 줄어듭니다.
3.2 핵심 솔루션 2: 파일 식별자 (FID)
경로 탐색 비용을 줄이기 위해, AFSv2는 파일 경로(Pathname) 대신 파일 식별자(FID: File Identifier) 를 도입했습니다. NFS의 파일 핸들과 유사한 개념입니다.
- FID의 구성:
Volume ID+File ID+Uniquifier(파일 삭제 후 재사용 시 구분용) - 동작 방식: 클라이언트는 경로를 한 번만 해석하고, 이후에는 FID를 사용하여 파일에 직접 접근합니다. 이로써 서버가 매번 디렉터리 트리를 탐색하는 오버헤드가 사라졌습니다.
3.3 AFSv2의 파일 접근 시나리오
클라이언트가 /home/remzi/notes.txt를 읽는 과정을 단계별로 살펴보겠습니다. (이미 /는 마운트 되어 있다고 가정)
- Open 요청: 응용 프로그램이
open("/home/remzi/notes.txt")를 호출합니다. - 디렉터리 탐색 및 캐싱:
- 클라이언트(Venus)는 서버(Vice)에게
home디렉터리의 내용을 요청합니다. - 서버는 내용을 보내주면서
home에 대한 콜백 을 설정합니다. - 클라이언트는
remzi디렉터리에 대해서도 동일하게 수행하고 콜백을 받습니다.
- 클라이언트(Venus)는 서버(Vice)에게
- 파일 Fetch:
- 마지막으로
notes.txt를 요청합니다. - 서버는 파일 전체 내용과 함께 콜백 을 설정해 줍니다.
- 클라이언트는 이를 로컬 디스크에 저장합니다.
- 마지막으로
- 로컬 연산: 이제
read(),write()는 오직 로컬 디스크의 파일에 대해서만 수행됩니다. 서버 통신은 '0'입니다. - 재접근 시: 나중에 다시 이 파일을 열 때, 클라이언트는 로컬에 캐시된 파일과 콜백 상태를 확인합니다. 콜백이 유효하다면 서버에 아무런 요청도 보내지 않고 즉시 로컬 파일을 엽니다.
이 구조 덕분에 AFSv2 서버는 단일 서버당 50대 이상의 클라이언트를 거뜬히 지원할 수 있게 되었습니다.
4. 캐시 일관성 (Cache Consistency) 모델
분산 파일 시스템에서 가장 골치 아픈 문제는 여러 사람이 동시에 같은 파일을 수정하면 어떻게 되는가? 입니다. AFS는 전체 파일 캐싱 과 콜백 을 기반으로 하기 때문에 일관성 모델이 NFS와는 확연히 다릅니다.
4.1 갱신 가시성 (Update Visibility)
- 언제 변경 내용이 서버에 반영되는가?
AFS에서는
close()시점입니다. 클라이언트가 로컬에서 아무리write()를 해도, 파일을 닫기 전까지는 서버나 다른 클라이언트는 그 내용을 알 수 없습니다. - 서버의 역할: 어떤 클라이언트가 변경된 파일을 서버에 저장(Store)하는 순간, 서버는 해당 파일에 대해 콜백을 가지고 있는 모든 다른 클라이언트에게 "콜백이 끊어졌다(Break Callback) 는 메시지를 보냅니다. 이제 다른 클라이언트들의 캐시는 '오래된 데이터(Stale)'가 됩니다.
4.2 동시성 제어: Last Writer Wins
서로 다른 두 클라이언트(A, B)가 동시에 같은 파일을 열고 수정한다면 어떻게 될까요?
- A가 파일을 엽니다(구버전).
- B가 파일을 엽니다(구버전).
- A가 수정 후 닫습니다 -> 서버에 A의 버전이 저장됩니다. (B의 콜백은 끊깁니다)
- B가 수정 후 닫습니다 -> 서버에 B의 버전이 저장됩니다. (A의 작업은 덮어씌워집니다)
결과적으로 마지막에 close()를 호출한 클라이언트의 내용이 최종 승리자 가 됩니다. 이를 Last Writer Wins (또는 Last Closer Wins) 정책이라고 합니다.
NFS가 블록 단위로 섞여서 파일이 깨질 수 있는 것(JPEG 파일의 윗부분은 A가, 아랫부분은 B가 쓰는 상황)과 달리, AFS는 파일 전체가 교체되므로 적어도 파일 내부의 데이터 정합성은 유지됩니다. (물론 A의 작업이 날아가는 건 어쩔 수 없습니다. 이를 막으려면 별도의 파일 잠금(Locking)을 써야 합니다.)
4.3 단일 머신 내의 일관성
재미있는 점은 같은 머신 내의 프로세스끼리 는 표준 UNIX 시맨틱을 따른다는 것입니다. 한 프로세스가 쓰면, 다른 프로세스는 close 하지 않아도 즉시 그 내용을 볼 수 있습니다. 이는 AFS 클라이언트가 로컬 파일 시스템을 통해 동작하기 때문입니다.
5. 크래시 복구 (Crash Recovery)
AFSv2의 콜백 모델은 성능은 좋지만, 상태(State) 를 관리해야 한다는 부담이 있습니다. "누가 어떤 파일의 콜백을 가지고 있는지"를 서버가 기억해야 하기 때문입니다.
5.1 클라이언트가 크래시 된 경우
클라이언트가 재부팅되면, 자신이 가지고 있던 콜백이 여전히 유효한지 알 수 없습니다. (재부팅 중에 서버가 콜백 해지 메시지를 보냈을 수도 있으니까요). 따라서 클라이언트는 재부팅 후 파일에 접근할 때 반드시 서버에 유효성 검사(Validation) 를 다시 수행해야 합니다.
5.2 서버가 크래시 된 경우
이게 더 큰 문제입니다. 서버는 메모리에 유지하던 콜백 목록을 모두 잃어버립니다. 서버가 재부팅되면 나는 아무것도 기억나지 않는다. 너희들의 캐시를 믿지 마라! 라고 모든 클라이언트에게 알려야 합니다. 클라이언트들은 이 소식을 듣거나, 주기적인 헬스 체크(Heartbeat) 실패를 감지하면, 자신의 모든 캐시를 무효화하고 다시 서버와 통신하여 상태를 갱신해야 합니다.
이는 상태를 저장하지 않는(Stateless) NFS가 서버 재부팅 후에도 아무 일 없던 것처럼 동작하는 것과는 대조되는 단점입니다. 하지만 AFS는 성능을 위해 복구의 복잡성을 감수 한 설계입니다.
6. 성능 분석: AFS vs NFS
AFS와 NFS는 설계 철학이 다르기 때문에 워크로드에 따라 승패가 갈립니다. 다음은 주요 시나리오별 성능 비교입니다. (가정: = 네트워크 지연, = 로컬 디스크 지연, = 로컬 메모리 지연)
6.1 AFS가 압도적으로 유리한 경우
- 대용량 파일의 순차적 재읽기 (Sequential Re-read of Large Files):
- NFS: 클라이언트 메모리에만 캐싱합니다. 파일이 메모리보다 크면, 다시 읽을 때마다 서버에서 네트워크를 통해 다시 가져와야 합니다. ( 발생)
- AFS: 로컬 디스크에 파일 전체가 캐싱되어 있습니다. 파일이 아무리 커도 로컬 디스크 속도()로 읽을 수 있습니다. 네트워크 트래픽은 0입니다.
- 결과: AFS 승.
6.2 AFS와 NFS가 비슷한 경우
- 작은 파일 읽기/쓰기:
- NFS도 작은 파일은 메모리에 캐싱하므로 빠릅니다. AFS도 메모리 캐시(Page Cache)의 도움을 받으므로 둘 다 속도를 냅니다.
- 첫 번째 읽기(Cold Cache): 둘 다 서버에서 가져와야 하므로 네트워크 속도()에 좌우됩니다. AFS가 디스크에 쓰는 오버헤드가 있을 것 같지만, OS의 버퍼링 덕분에 성능 차이는 미미합니다.
6.3 AFS가 불리한 경우
- 대용량 파일의 순차적 덮어쓰기 (Sequential Overwrite):
- NFS: 단순히 블록을 덮어쓰면 됩니다.
- AFS: 파일을 쓰려면 먼저
open을 해야 하는데, AFS는open시점에 파일 전체를 서버에서 가져옵니다(Fetch). 어차피 다 덮어쓸 파일인데 굳이 네트워크를 써서 가져오는 낭비가 발생합니다.
- 파일의 일부만 작은 크기로 갱신:
- AFS는 작은 변경이 있어도
close시점에 파일 전체 를 서버로 전송해야 합니다. 이는 막대한 대역폭 낭비입니다. 반면 NFS는 변경된 블록만 보냅니다.
- AFS는 작은 변경이 있어도
7. 기타 기능 및 워크로드 가정
7.1 추가 기능들
AFS는 단순한 파일 공유를 넘어 실제 캠퍼스 환경에서 사용하기 위한 기능들을 갖췄습니다.
- 글로벌 네임스페이스 (Global Namespace): 모든 클라이언트에서
/afs/로 시작하는 동일한 경로로 파일에 접근합니다. (NFS는 마운트 포인트가 제각각일 수 있음) - 보안 (Security): 커버로스(Kerberos) 기반의 인증과, 사용자별로 파일을 숨길 수 있는 접근 제어 목록(ACL)을 제공합니다.
- 볼륨(Volume) 관리: 관리자가 서버 간에 데이터를 쉽게 이동시킬 수 있는 '볼륨' 개념을 도입했습니다.
7.2 워크로드에 대한 가정
AFS 설계자들은 파일은 잘 공유되지 않으며, 한 번 열리면 전체를 순차적으로 읽는다 라고 가정했습니다. 이 가정하에서는 AFS의 설계가 완벽합니다. 하지만, 데이터베이스 처럼 여러 사용자가 동시에 파일의 임의 위치(Random Access)를 지속적으로 갱신하거나, 로그 파일처럼 조금씩 계속 추가하는(Append) 작업에는 AFS가 적합하지 않습니다.
8. 결론: AFS의 유산
AFS는 분산 시스템 역사에서 기념비적인 시스템입니다. 비록 현재는 NFSv4나 클라우드 스토리지, CIFS(SMB) 등에 밀려 주류에서 사라졌지만, 그가 남긴 유산은 여전합니다.
- 확장성의 핵심은 서버 부하 감소: 클라이언트와의 통신을 최소화해야만 시스템이 확장될 수 있음을 증명했습니다.
- 강한 일관성보다는 실용적인 일관성: 'Last Writer Wins'와 'Open-to-Close' 일관성 모델은 이해하기 쉽고 구현하기 효율적입니다.
- 최신 프로토콜의 진화: 아이러니하게도, NFSv4 는 상태(State) 개념과 유사한 기능들을 도입하면서 점점 AFS를 닮아가고 있습니다.
개발자로서 분산 시스템을 설계할 일이 있다면, AFS가 던진 질문을 기억해야 합니다. "클라이언트가 계속 물어보게 할 것인가(Polling), 아니면 변경될 때 알려줄 것인가(Callback/Notification)?" 이 선택이 시스템의 확장성을 결정짓는 열쇠가 될 것입니다.
요약 (Takeaway):
- AFSv1: 전체 파일 캐싱 시도 -> 경로 탐색과 TestAuth 폭주로 실패.
- AFSv2: 콜백(Callback) 도입으로 서버 부하 최소화, FID 로 탐색 비용 제거 -> 확장성 확보.
- 일관성:
close()시점에 서버 반영, Last Closer Wins. - 성능: 대용량 파일 재읽기는 최강(로컬 디스크 활용), 잦은 부분 수정이나 덮어쓰기는 비효율적.
- 교훈: 측정하라(Measure it), 그리고 서버의 상태 관리(Stateful)는 복잡하지만 성능을 위한 강력한 무기다.
